跳到主要内容

TypeScript keyof 和 泛型的使用

写 TypeScript 什么都好,就是它的类型系统有时候太麻烦了,所以这篇笔记记录一下遇到的各种类型问题如何处理

处理类型的常用操作符一览表

关键字作用
instanceof实例判断
typeof类型判断
as类型强制转换
is断言返回布尔类型
?条件类型
keyof键名索引
in映射
infer声明待推断的类型
<>泛型
type别名
|联合类型
&交叉类型

keyof 关键字的使用

该操作符可以用于获取某种类型的所有键,其返回类型是联合类型

interface Person {
name: string;
age: number;
}

type K1 = keyof Person; // "name" | "age"
type K2 = keyof Person[]; // "length" | "toString" | "pop" | "push" | "concat" | "join"
type K3 = keyof { [x: string]: Person }; // string | number

遍历对象的属性⭐

在 TypeScript 里面,当需要遍历对象的时候,经常就会遇到下图所示的错误提示。

function test (foo: object) {
for (let key in foo) {
console.log(foo[key]); // typescript错误提示
// do something
}
}

解决方法:

把对象声明 as any

function test (foo: object) {
for (let key in foo) {
console.log((foo as any)[key]); // 报错消失
// do something
}
}

但是这个方法有点像是直接绕过了 typescript 的校验机制,失去了使用 typescript 的初心,这不是我们想要的。

给对象声明一个接口⭐

interface SimpleKeyValueObject {
[key: string]: any
}

function test (foo: SimpleKeyValueObject) {
for (let key in foo) {
console.log(foo[key]); // 报错消失
// do something
}
}

这个可以针对比较常见的对象类型,特别是一些工具方法。

但是这个不够骚,能不能再厉害点呢~

使用泛型

function test<T extends object> (foo: T) {
for (let key in foo) {
console.log(foo[key]); // 报错消失
// do something
}
}

当我们需要对 foo 的 key 进行约束时,我们可以使用下面的第 4 种方法。

使用 keyof

interface Ifoo{
name: string;
age: number;
weight: number;
}

function test (opt: Ifoo) {
let key: (keyof Ifoo);
for (key in opt) {
console.log(opt[key]); // 报错消失
// do something
}
}

Intersection Types(交叉类型)

交叉类型是一种将多种类型组合为一种类型的方法。 这意味着你可以将给定的类型 A 与类型 B 或更多类型合并,并获得具有所有属性的单个类型。

type LeftType = {
id: number;
left: string;
};

type RightType = {
id: number;
right: string;
};

type IntersectionType = LeftType & RightType;

function showType(args: IntersectionType) {
console.log(args);
}

showType({ id: 1, left: 'test', right: 'test' });
// Output: {id: 1, left: "test", right: "test"}

IntersectionType 组合了两种类型-LeftType和RightType,并使用 & 符号形成了交叉类型。

Union Types(联合类型)

联合类型使你可以赋予同一个变量不同的类型

type UnionType = string | number;

function showType(arg: UnionType) {
console.log(arg);
}

showType('test');
// Output: test

showType(7);
// Output: 7

Pick 从类型抽取属性为新类型

Pick 方法允许你从一个已存在的类型 T 中选择一些属性作为 K,从而创建一个新类型

抽取一个类型/接口中的一些子集作为一个新的类型

T 代表要抽取的对象 K 有一个约束:一定是来自 T 所有属性字面量的联合类型

新的 类型/属性 一定要从 K 中选取

源码实现

type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
  • T 是要从中选择元素的类型
  • K 是要选择的属性(可以使使用联合类型来选择多个字段)

使用例:

interface PickType {
id: number;
firstName: string;
lastName: string;
}

function showType(args: Pick<PickType, 'firstName' | 'lastName'>) {
console.log(args);
}

showType({ firstName: 'John', lastName: 'Doe' });
// Output: {firstName: "John"}

showType({ id: 3 });
// Error: Object literal may only specify known properties, and 'id' does not exist in type 'Pick<PickType, "firstName" | "lastName">'

Omit 删除部分属性

Omit 的作用与 Pick 类型正好相反。 不是选择元素,而是从类型 T 中删除 K 个属性。

interface PickType {
id: number;
firstName: string;
lastName: string;
}

function showType(args: Omit<PickType, 'firstName' | 'lastName'>) {
console.log(args);
}

showType({ id: 7 });
// Output: {id: 7}

showType({ firstName: 'John' });
// Error: Object literal may only specify known properties, and 'firstName' does not exist in type 'Pick<PickType, "id">'

Extract 取类型的交集

Extract 提取 T 中可以赋值给 U 的类型(取交集)

Extract 允许你通过选择两种不同类型中的共有属性来构造新的类型。 也就是从 T 中提取所有可分配给 U 的属性。

interface FirstType {
id: number;
firstName: string;
lastName: string;
}

interface SecondType {
id: number;
address: string;
city: string;
}

type ExtractType = Extract<keyof FirstType, keyof SecondType>;
// Output: "id"

在上面的代码中,FirstType 接口和 SecondType 接口,都存在 id:number 属性。 因此,通过使用 Extract,即提取出了新的类型 {id:number}

Exclude 排除交集

Exclude 与上相反,从 T 中剔除可以赋值给 U 的类型。

与 Extract 不同,Exclude 通过排除两个不同类型中已经存在的共有属性来构造新的类型。 它会从 T 中排除所有可分配给 U 的字段。

interface FirstType {
id: number;
firstName: string;
lastName: string;
}

interface SecondType {
id: number;
address: string;
city: string;
}

type ExcludeType = Exclude<keyof FirstType, keyof SecondType>;

// Output; "firstName" | "lastName"

Mapped Types(旧映射为新类型)⭐

映射类型允许你从一个旧的类型,生成一个新的类型。

这里先来看下上面的

type StringMap<T> = {
[P in keyof T]: string;
};

function showType(arg: StringMap<{ id: number; name: string }>) {
console.log(arg);
}

showType({ id: 1, name: 'Test' });
// Error: Type 'number' is not assignable to type 'string'.

showType({ id: 'testId', name: 'This is a Test' });
// Output: {id: "testId", name: "This is a Test"}

StringMap 会将传入的任何类型转换为字符串。 就是说,如果我们在函数 showType() 中使用它,则接收到的参数必须是字符串-否则,TypeScript 将引发错误。

错误场景1

开发中使用 typescript 的时候,经常会遇到使用 Object.keys 这个方法报错的情况,报错如下:

补充:Object.keys() 方法会返回一个由一个给定对象的自身可枚举属性组成的数组,数组中属性名的排列顺序和正常循环遍历该对象时返回的顺序一致 。

// 数组
var obj = {'a':'123','b':'345'};
console.log(Object.keys(obj)); //['a','b']

而到了 TS 会发生如下报错

var foo = {
a: '1',
b: '2'
}

var getPropertyValue = Object.keys(foo).map(item => foo[item]) // 这里会有typescript的错误提示

解决方案:通过 keyof 的方式可以获取 ts 类型的属性 key 的值

var foo = {
a: '1',
b: '2'
}
// 这里 typeof foo => foo的类型 等同于 interface Foo { a: string; b: string; }
// typeof foo === Foo,这里只所以用 typeof foo,因为这样方便,对于不想写interface的直接量对象很容易获取它的类型
// keyof typeof foo 这里只获取 Foo 的类型的 key 值,注意这个 keyof 后面一定是 typescript 的类型
type FooType = keyof typeof foo;

var getPropertyValue = Object.keys(foo).map(item => foo[item as FooType])

错误场景2

var foo = {
a: '1',
b: '2'
}

function getPropertyValue(obj, key) { // 这里也会提示obj会有any类型
return obj[key]
}

可以通过以下的方法解决:

var foo = {
a: '1',
b: '2'
}
// 这里声明了两个泛型 T 和 K
// T 代表函数第一个参数的类型,K 代表函数第二个参数的类型这个类型指向第一个参数类型中包含的key的值
function getPropertyValue<T, K extends keyof T>(obj:T, key:K):T[K] {
return obj[key]
}
getPropertyValue(foo, 'a')

Reference

TypeScript小状况之遍历对象属性 TypeScript 高级类型清单(附 demo)